cover

优化 Python 应用容器镜像构建

本文介绍了使用 Docker 多阶段构建和 Python Wheel 优化 Python 应用容器镜像构建方法。

2023-09-17

heading

最简单的方式

最近尝试使用 LangChain 实现了一个简单的 LLM 应用。在这个应用中用到了 flask 作为后端框架,开始时使用如下的

Dockerfile
构建应用镜像:

WORKDIR /app COPY requirements.txt requirements.txt RUN pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple \ pip install -r requirements.txt COPY . . CMD ["gunicorn", "dzyqa_api:create_app()", "--bind=0.0.0.0:7860", "--workers=5", "--threads=2", "--timeout=300"]

这个镜像文件使用一个基础 Python 镜像作为应用运行环境,安装依赖后运行

gunicorn
。在服务器上(双核 4G)编译花费了 170 秒左右,镜像文件大小 1.5G。

为了优化这个镜像文件,尝试使用 slim 作为基础镜像,但由于在 slim 镜像中应用的某些依赖无法顺利编译所以只能放弃这种方法。

heading

使用多阶段构建和 Wheel 进行优化

造成应用镜像无法瘦身的原因是应用的依赖文件无法在 slim 或 alpine 环境中编译,因此如果能提前编译依赖文件,是否就能够在 slim 基础镜像中运行应用呢?

想到这里就想起了以往在 Windows 上开发 Python 应用时,如果出现无法编译的依赖时,都会去 这里 找找是否有编译好的依赖文件。

至此,优化的思路就很清晰了:

  • 通过使用多阶段构建将应用的"构建"和"运行"环境分开;
  • 使用 wheel 文件将应用运行的必要依赖放置到"运行"环境中。

下面是优化后的

Dockerfile

FROM python:3.10.12-bookworm as builder # "编译"阶段,编译 wheel 文件 WORKDIR /app COPY requirements.txt requirements.txt RUN pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple && \ pip wheel --wheel-dir=/app/wheelhouse -r requirements.txt FROM python:3.10.12-slim-bookworm as final # "运行"阶段,从"编译"阶段复制依赖文件,然后运行应用 WORKDIR /app COPY . . COPY --from=builder /app/wheelhouse /app/wheelhouse RUN pip install -r requirements.txt --no-index --find-links /app/wheelhouse CMD ["gunicorn", "dzyqa_api:create_app()", "--bind=0.0.0.0:7860", "--workers=5", "--threads=2", "--timeout=300"]

使用这个

Dockerile
进行编译,在相同的机器上,花费的时间是 137s,镜像大小为 700MB。对比之前使用的镜像文件,提升的幅度还是很可观的。

heading

补充内容

下面补充一些 Docker 多阶段构建和 Python Wheel 文件的说明。

heading

Docker 多阶段构建 Multi-stage build

在 Docker 中,构建一个容器镜像时,通常需要包括应用程序的所有依赖项和构建工具。这导致了镜像变得庞大且资源消耗高,尤其是在生产环境中。通过使用多阶段构建使得我们可以在构建镜像的不同阶段使用不同的基础镜像以满足该阶段的要求,但只有最后一个构建阶段的内容会被包含在应用容器镜像中。通过这种方式能够实现在应用镜像中只包含应用程序运行所必须的依赖,从而使得应用镜像能够更小巧、更高效。

heading

Python Wheel 二进制分发格式

Wheel 文件是 Python 的二进制分发格式,在 PEP427 中提出。使用 wheel 文件的主要目的是提高 Python 软件包的安装效率和可移植性。通过提供预编译的二进制文件,wheel 文件能够快速安装软件包,而不需要重新编译源代码,从而节省了时间和资源。此外,它还有助于减少软件包依赖的复杂性,使软件包在不同平台上更容易分发和安装。